-
Notifications
You must be signed in to change notification settings - Fork 9.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(core): Custom session timeout and refresh configuration #8342
feat(core): Custom session timeout and refresh configuration #8342
Conversation
Rename `JwtToken.expiresIn` to `JwtToken.expiresInSeconds` to make it clear what unit it's supposed to be. Also it's now seconds and not milliseconds. Reason for that is that seconds are a base unit of the SI, which makes it a good default. Also it's what the JWTs expect in `expireIn` and what the cookie Max-Age property expects. Until now the function returned milliseconds and that was passed into the Max-Age property in `auth/jwt.issueCookie`. This caused no real issue, but it meant that the JWT would always expire before the cookie.
no need to have it in the beforeEach
Additionally I had to wrangle the types a bit. I want to call `await refreshExpiringCookie(...)` in the unit tests, but to do this without TS complaining the type needs account for the function being async. Right now adding the type to the variable binding hides that fact. My workaround for that is to let TS infer the type of `refreshExpiringCookie`. That lead to TS not being able to infer the types of `res` and `next`. To fix that I used `satisfied`. Happy to apply other solutions to this like extending the RequestHandler type and creating an `AsyncRequestHandler` or adding an `Async` type helper and using that. ```ts type Async<T extends (...args: unknown[]) => unknown> = ( ...args: Parameters<T> ) => Promise<ReturnType<T>>; ```
…r than jwtSessionDurationHours
1. use Time constant for more readable time conversion 2. use jest-mock-extended to create users and requests 3. use type guards and jest.fail to remove unecessary assertions
I'll add this as a comment in the review, where we can agree on where the warning should be emitted.
f68fea3
to
24472c1
Compare
// Can't use the logger here, because it depends on the config. | ||
// I could validate it in `Start.init()`, but that feels like the wrong seperation of concerns | ||
// I could also do it in `refreshExpiringCookie`, but that would then print the message on every request. | ||
console.warn( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't use the `Container.get(Logger) here, because it depends on the config, which creates a dependency cycle.
Alternative:
- I could validate it in
Start.init()
, but that feels like the wrong separation of concerns - I could also do it in
refreshExpiringCookie
, but that would then print the message on every request.
* Please amend conversions as necessary. | ||
* Eventually this will superseed `TIME` above | ||
*/ | ||
export const Time = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I talked with @ivov about this and he likes it.
I would make a refactor PR removing TIME
and replacing it with this.
TIME only allowed converting everything into milliseconds, but I was working with JWT and cookies which all expect seconds, so I thought this here is a bit more versatile.
We can also add a library for this, but the problem seems a bit too trivial to justify adding third party code to make the conversions more readable.
Feel free to disagree and comment.
if (jwtRefreshTimeoutHours === 0) { | ||
const jwtSessionDurationHours = config.get('userManagement.jwtSessionDurationHours'); | ||
|
||
jwtRefreshTimeoutMilliSeconds = Math.floor(jwtSessionDurationHours * 0.25 * 60 * 60 * 1000); | ||
} else { | ||
jwtRefreshTimeoutMilliSeconds = Math.floor(jwtRefreshTimeoutHours * 60 * 60 * 1000); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to use a ternary here instead. Just let me know.
// if cookie expires in < 3 days, renew it. | ||
await issueCookie(res, req.user); | ||
} | ||
} | ||
next(); | ||
}; | ||
}) satisfies RequestHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to wrangle the types a bit. I want to call
await refreshExpiringCookie(...)
in the unit tests, but to do this
without TS complaining about awaiting a non-promise the type needs show that this function is async.
Right now adding the type to the variable binding hides that fact. RequestHandler
is synchronous.
My workaround for that is to let TS infer the type of refreshExpiringCookie
. That lead to TS not being able to infer the types of res
and next
anymore. So to fix that I used satisfied
, which allows TS to infer the types of the arguments and also infer the type of the function as a whole without the need to create a new type for async request handlers.
Happy to apply other solutions to this like extending the RequestHandler
type and creating an AsyncRequestHandler
or adding an Async
type
helper and using that.
type Async<T extends (...args: unknown[]) => unknown> = (
...args: Parameters<T>
) => Promise<ReturnType<T>>;
Let me know what you think.
packages/cli/src/Interfaces.ts
Outdated
@@ -667,7 +667,7 @@ export interface ILicensePostResponse extends ILicenseReadResponse { | |||
|
|||
export interface JwtToken { | |||
token: string; | |||
expiresIn: number; | |||
expiresInSeconds: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the context of auth tokens, I've never seen an expiresIn
that's not in seconds.
While I do see the additional clarity of mentioning seconds here, I don't think we need to rename the property itself.
I've seen an uptick in the number of long descriptive variable names lately , and I feel like this makes code increasingly harder to read.
IMO variable names should try to be as simple as possible, and all additional relevant metadata should move to JSDocs.
In this particular case, I'd prefer if renamed this back, and add a more descriptive JSDoc instead. That was anyone inspecting the code will still know that this is in seconds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to change it back, but some background before I do that.
I've never seen an expiresIn that's not in seconds.
I actually thought exactly the same thing. So I assumed this is in seconds:
n8n/packages/cli/src/auth/jwt.ts
Lines 39 to 41 in b6d7757
const signedToken = Container.get(JwtService).sign(payload, { | |
expiresIn: expiresIn / 1000 /* in seconds */, | |
}); |
And it is.
I also thought maxAge
is in seconds:
n8n/packages/cli/src/auth/jwt.ts
Lines 88 to 92 in b6d7757
res.cookie(AUTH_COOKIE_NAME, userData.token, { | |
maxAge: userData.expiresIn, | |
httpOnly: true, | |
sameSite: 'lax', | |
}); |
Because that's what it is in the browser: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#max-agenumber
And on master we currently pass milliseconds. So I thought I found a bug and fixed it in this PR.
Now I tried it out locally and it turns out express expects milliseconds and converts it internally:
https://expressjs.com/en/api.html#res.cookie
I'm happy to change it back, but I hope that shined some light on why I thought it may be helpful. If express would call their option maxAgeMilliseconds
I would have not introduced a bug in this PR 😄
I'll fix the bug first that I have in the PR. Let me know what you think and if you still want me to revert the name change then I will.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to report that there was a bug in the duration, glad you've already figured it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get what you are saying and I agree that it's important to be clear about what unit a variable uses. My point was that we should use more JSDoc for that, and be leaner with our variable names. I've seen long and descriptive names get out of hand in old projects, and that makes code much harder to read because people put more details about what a variable does by putting all the details in the name.
We should instead send PRs (whenever possible) to add JSDoc on the packages we use to have the docs in our editors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. The JSDoc works well enough for me, so I changed the interface back and documented it via JSDoc.
Good thinking with amending the DT types for express.
I hope your DT PR goes through 🤞🏾
✅ All Cypress E2E specs passed |
3 flaky tests on run #3854 ↗︎
Details:
5-ndv.cy.ts • 1 flaky test
17-sharing.cy.ts • 1 flaky test
24-ndv-paired-item.cy.ts • 1 flaky test
Review all test suite changes for PR #8342 ↗︎ |
✅ All Cypress E2E specs passed |
Got released with |
Summary
This allows configuring the session timeout and refresh via environment variables.
You can test it by starting n8n like this:
0.005 hours is 18 seconds and -1 means that the auto refresh of the cookie/JWT is disabled.
So you can log in, navigate around. Wait 18 seconds. Refresh and see that you're logged out.
If you want to test the auto refresh you can do this:
This will make the session last for 18 seconds and when less than 9 seconds are remaining it will issue a new cookie/JWT.
Reviewing this commit by commit can be easier. I mostly added tests before making changes, to make sure I don't break old behaviour.
Related tickets and issues
https://linear.app/n8n/issue/PAY-1182/custom-session-timeout-configuration
https://linear.app/n8n/issue/DOC-734/add-custom-session-timeout-setting-to-environment-variables-page
n8n-io/n8n-docs#1849
Review / Merge checklist
(no-changelog)
otherwise. (conventions)